/*
 * Copyright (c) 2011, 2024 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     tware - initial implementation as part of extensibility feature
//     01/11/2013-2.5 Guy Pelletier
//       - 389090: JPA 2.1 DDL Generation Support
//     01/16/2013-2.5 Guy Pelletier
//       - 389090: JPA 2.1 DDL Generation Support
//     02/04/2013-2.5 Guy Pelletier
//       - 389090: JPA 2.1 DDL Generation Support
//     05/26/2016-2.7 Tomas Kraus
//       - 494610: Session Properties map should be Map<String, Object>
//     08/23/2023: Tomas Kraus
//       - New Jakarta Persistence 3.2 Features
package org.eclipse.persistence.internal.jpa;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;

import jakarta.persistence.Cache;
import jakarta.persistence.EntityGraph;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.FlushModeType;
import jakarta.persistence.PersistenceException;
import jakarta.persistence.PersistenceUnitTransactionType;
import jakarta.persistence.PersistenceUnitUtil;
import jakarta.persistence.Query;
import jakarta.persistence.SchemaManager;
import jakarta.persistence.SynchronizationType;
import jakarta.persistence.TypedQueryReference;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.metamodel.Attribute;
import jakarta.persistence.metamodel.Metamodel;
import org.eclipse.persistence.config.ReferenceMode;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.descriptors.VersionLockingPolicy;
import org.eclipse.persistence.exceptions.PersistenceUnitLoadingException;
import org.eclipse.persistence.internal.descriptors.OptimisticLockingPolicy;
import org.eclipse.persistence.internal.indirection.IndirectionPolicy;
import org.eclipse.persistence.internal.localization.ExceptionLocalization;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.internal.sessions.DatabaseSessionImpl;
import org.eclipse.persistence.internal.sessions.coordination.MetadataRefreshCommand;
import org.eclipse.persistence.jpa.JpaEntityManagerFactory;
import org.eclipse.persistence.mappings.DatabaseMapping;
import org.eclipse.persistence.mappings.ForeignReferenceMapping;
import org.eclipse.persistence.queries.AttributeGroup;
import org.eclipse.persistence.queries.DatabaseQuery;
import org.eclipse.persistence.queries.FetchGroupTracker;
import org.eclipse.persistence.queries.ObjectLevelReadQuery;
import org.eclipse.persistence.queries.ReadQuery;
import org.eclipse.persistence.sessions.DatabaseSession;
import org.eclipse.persistence.sessions.Session;
import org.eclipse.persistence.sessions.UnitOfWork.CommitOrderType;
import org.eclipse.persistence.sessions.broker.SessionBroker;
import org.eclipse.persistence.sessions.coordination.CommandManager;
import org.eclipse.persistence.sessions.factories.SessionManager;
import org.eclipse.persistence.sessions.server.Server;
import org.eclipse.persistence.sessions.server.ServerSession;

/**
 * Wraps our implementation of EntityManagerFactory
 * Most operations are forwarded to the delegate.  This wrapper is used to enable
 * the refreshMetadata functionality which allows you to switch the underlying metadata for
 * an EMF after deploy time.
 * @author tware
 *
 */
public class EntityManagerFactoryImpl implements EntityManagerFactory, PersistenceUnitUtil, JpaEntityManagerFactory {

    protected EntityManagerFactoryDelegate delegate;

    /**
     * Returns the id of the entity. A generated id is not guaranteed to be
     * available until after the database insert has occurred. Returns null if
     * the entity does not yet have an id
     *
     * @return id of the entity
     * @throws IllegalArgumentException if the given object is not an instance
     *         of an entity class belonging to the persistence unit
     */
    public static Object getIdentifier(Object entity, AbstractSession session) {
        ClassDescriptor descriptor = session.getDescriptor(entity);
        if (descriptor == null) {
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage(
                    "jpa_persistence_util_non_persistent_class", new Object[] { entity }));
        }
        if (descriptor.getCMPPolicy() != null) {
            return descriptor.getCMPPolicy().createPrimaryKeyInstance(entity, session);
        } else {
            // 308950: Alternatively, CacheImpl.getId(entity) handles a null CMPPolicy case for weaved and unweaved domain object
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage("jpa_persistence_util_non_persistent_class", new Object[] { entity }));
        }
    }

    /**
     * Returns the version of provided entity instance.
     *
     * @param entity the entity instance
     * @param session database session
     * @param <T> type of the version
     * @return version of the entity instance
     */
    static <T> T getVersion(Object entity, AbstractSession session) {
        ClassDescriptor descriptor = session.getDescriptor(entity);
        if (descriptor == null) {
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage(
                    "jpa_persistence_util_get_version_non_persistent_class", new Object[] { entity }));
        }
        OptimisticLockingPolicy lockingPolicy = descriptor.getOptimisticLockingPolicy();
        if (lockingPolicy instanceof VersionLockingPolicy versionLockingPolicy) {
            return versionLockingPolicy.getVersion(entity);
        }
        throw new IllegalArgumentException(ExceptionLocalization.buildMessage(
                "jpa_persistence_util_get_version_no_version_in_class", new Object[] { entity }));
    }

    /**
     * Load the persistent state of an entity belonging to the
     * persistence unit and to an open persistence context.
     * After this method returns, {@link #isLoaded(Object, AbstractSession)} must
     * return true with the given entity instance.
     *
     * @param entity entity instance to be loaded
     * @param session database session
     * @throws IllegalArgumentException if the given object is not an instance
     *         of an entity class belonging to the persistence unit
     */
    static void load(Object entity, AbstractSession session) {
        ClassDescriptor descriptor = session.getDescriptor(entity);
        if (descriptor != null) {
            List<DatabaseMapping> mappings = descriptor.getMappings();
            for (DatabaseMapping mapping : mappings) {
                if (!mapping.isLazy() && !isLoaded(entity, mapping.getAttributeName(), mapping)) {
                    load(entity, mapping.getAttributeName(), mapping);
                }
            }
        } else {
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage(
                    "jpa_persistence_util_non_persistent_class", new Object[] {entity}));
        }
    }

    /**
     * Load the persistent value of a given persistent attribute
     * of an entity belonging to the persistence unit and to an
     * open persistence context.
     * After this method returns, {@link #isLoaded(Object, String, AbstractSession)}
     * must return true with the given entity instance and attribute.
     *
     * @param entity entity instance to be loaded
     * @param attributeName the name of the attribute to be loaded
     * @param session database session
     * @throws IllegalArgumentException if the given object is not an instance
     *         of an entity class belonging to the persistence unit
     */
    static void load(Object entity, String attributeName, AbstractSession session) {
        ClassDescriptor descriptor = session.getDescriptor(entity);
        if (descriptor != null) {
            DatabaseMapping mapping = descriptor.getMappingForAttributeName(attributeName);
            if (mapping != null) {
                load(entity, attributeName, mapping);
            }
        } else {
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage(
                    "jpa_persistence_util_non_persistent_class", new Object[] {entity}));
        }
    }

    /**
     * Load the persistent value of a given persistent attribute
     * of an entity belonging to the persistence unit with a given mapping.
     *
     * @param entity entity instance to be loaded
     * @param attributeName the name of the attribute to be loaded
     * @param mapping database mapping metadata of the attribute
     */
    private static void load(Object entity, String attributeName, DatabaseMapping mapping) {
        if (mapping.isForeignReferenceMapping()) {
            Object value = mapping.getAttributeValueFromObject(entity);
            IndirectionPolicy policy = ((ForeignReferenceMapping) mapping).getIndirectionPolicy();
            if (!policy.objectIsInstantiated(value)) {
                policy.instantiateObject(entity, value);
            }
        } else if (entity instanceof FetchGroupTracker tracker) {
            if (!tracker._persistence_isAttributeFetched(attributeName)) {
                EntityManagerImpl.processUnfetchedAttribute(tracker, attributeName);
            }
        }
    }

    /**
     * Determine the load state of an entity belonging to the
     * persistence unit. This method can be used to determine the
     * load state of an entity passed as a reference. An entity is
     * considered loaded if all attributes for which
     * {@link jakarta.persistence.FetchType#EAGER} has been specified have been loaded.
     * <p> The {@link #isLoaded(Object, String)} method should be
     * used to determine the load state of an attribute. Not doing
     * so might lead to unintended loading of state.
     *
     * @param entity entity instance whose load state is to be determined
     * @param session database session
     * @return Value of {@code true} if the given entity has been loaded
     *         or {@code false} otherwise.
     */
    public static Boolean isLoaded(Object entity, AbstractSession session) {
        ClassDescriptor descriptor = session.getDescriptor(entity);
        if (descriptor == null) {
            return null;
        }
        List<DatabaseMapping> mappings = descriptor.getMappings();
        for (DatabaseMapping mapping : mappings) {
            if (!mapping.isLazy() && !isLoaded(entity, mapping.getAttributeName(), mapping)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Determine the load state of a given persistent attribute
     * of an entity belonging to the persistence unit.
     *
     * @param entity entity instance containing the attribute
     * @param attributeName name of attribute whose load state is to be determined
     * @param session database session
     * @return Value of {@code true} if the given attribute has been loaded
     *         or {@code false} otherwise.
     */
    public static Boolean isLoaded(Object entity, String attributeName, AbstractSession session) {
        ClassDescriptor descriptor = session.getDescriptor(entity);
        if (descriptor == null) {
            return null;
        }
        if (descriptor.hasFetchGroupManager()){
            if (!descriptor.getFetchGroupManager().isAttributeFetched(entity, attributeName)){
                return false;
            }
        }
        DatabaseMapping mapping = descriptor.getMappingForAttributeName(attributeName);
        if (mapping == null) {
            return null;
        }
        return isLoaded(entity, attributeName, mapping);
    }

    /**
     * Check whether a named attribute on a given entity with a given mapping
     * has been loaded.
     * This method will check the value holder or indirect collection for LAZY
     * ForeignReferenceMappings to see if has been instantiated and otherwise
     * check the fetch group.
     *
     * @param entity entity instance containing the attribute
     * @param attributeName name of attribute whose load state is to be determined
     * @param mapping database mapping metadata of the attribute
     * @return Value of {@code true} if the given attribute has been loaded
     *         or {@code false} otherwise.
     */
    public static boolean isLoaded(Object entity, String attributeName, DatabaseMapping mapping) {
        if (mapping.isForeignReferenceMapping()) {
            if (mapping.isLazy()) {
                Object value = mapping.getAttributeValueFromObject(entity);
                IndirectionPolicy policy = ((ForeignReferenceMapping) mapping).getIndirectionPolicy();
                return policy.objectIsInstantiated(value);
            }
        }

        if (entity instanceof FetchGroupTracker) {
            return ((FetchGroupTracker) entity)._persistence_isAttributeFetched(attributeName);
        } else {
            return true;
        }
    }

    /**
     * Check whether the given entity belonging to the persistence
     * unit and to an open persistence context is an instance of the
     * given entity class, or false otherwise.
     * This method may, but is not required to, load the given entity
     * by side effect.
     *
     * @param entity entity instance
     * @param entityClass an entity class belonging to the persistence unit
     * @param session database session
     * @return Value of {@code true} if the given entity is an instance
     *         of the given entity class or {@code false} otherwise.
     * @throws IllegalArgumentException if the given object is not an instance
     *         of an entity class belonging to the persistence unit
     */
    static boolean isInstance(Object entity, Class<?> entityClass, AbstractSession session) {
        // Just validate that entity belongs to current PU
        ClassDescriptor descriptor = session.getDescriptor(entity);
        if (descriptor == null) {
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage(
                    "jpa_persistence_util_non_persistent_class", new Object[] {entity}));
        }
        return entityClass.isInstance(entity);
    }

    /**
     * Return the concrete entity class if the given entity belonging
     * to the persistence unit and to an open persistence context.
     * This method may, but is not required to, load the given entity
     * by side effect.
     *
     * @param entity  entity instance
     * @param session database session
     * @return an entity class belonging to the persistence unit
     * @throws IllegalArgumentException if the given object is not an instance
     *         of an entity class belonging to the persistence unit
     */
    static <T> Class<? extends T> getClass(T entity, AbstractSession session) {
        ClassDescriptor descriptor = session.getDescriptor(entity);
        if (descriptor == null) {
            throw new IllegalArgumentException(ExceptionLocalization.buildMessage(
                    "jpa_persistence_util_non_persistent_class", new Object[] {entity}));
        }
        return descriptor.getJavaClass();
    }

    /**
     * Will return an instance of the Factory. Should only be called by
     * EclipseLink.
     *
     */
    public EntityManagerFactoryImpl(AbstractSession serverSession) {
        delegate = new EntityManagerFactoryDelegate(serverSession, this);
    }

    public EntityManagerFactoryImpl(EntityManagerSetupImpl setupImpl, Map properties) {
        delegate = new EntityManagerFactoryDelegate(setupImpl, properties, this);
    }

    /**
     * Create a dynamic persistence unit which does not use the persistence.xml.
     * Instead all configuration is driven from the provided persistence unit
     * properties and descriptors.
     */
    public EntityManagerFactoryImpl(String persistenceUnitName, Map<String, Object> properties, List<ClassDescriptor> descriptors) {
        delegate = new EntityManagerFactoryDelegate(persistenceUnitName, properties, descriptors, this);
    }

    /**
     * ADVANCED:
     * Re-bootstrap this factory.  This method will rebuild the EntityManagerFactory.  It should be used
     * in conjunction with a MetadataSource to allow mappings to be changed in a running system.  All existing
     * EntityMangers will continue to function with the old metadata, but new factories will use the new metadata.
     */
    @Override
    public void refreshMetadata(Map<String, Object> properties){
        EntityManagerSetupImpl setupImpl = delegate.getSetupImpl();
        if (setupImpl == null){
            throw PersistenceUnitLoadingException.cannotRefreshEntityManagerFactoryCreatedFromSession(delegate.getServerSession().getName());
        }
        String sessionName = setupImpl.getSessionName();
        Map<String, Object> existingProperties = delegate.getProperties();
        Map<String, Object> deployProperties = new HashMap<>(existingProperties);
        if (properties != null){
            deployProperties.putAll(properties);
        }
        EntityManagerSetupImpl newSetupImpl = setupImpl.refreshMetadata(properties);
        EntityManagerFactoryDelegate oldDelegate = delegate;
        delegate = new EntityManagerFactoryDelegate(newSetupImpl, deployProperties, this);
        // This code has been added to allow validation to occur without actually calling createEntityManager
        // RCM refresh command requires the DEPLOY_ON_STARTUP property is set to true so the listener can be added to the session.
        try{
            if (newSetupImpl.shouldGetSessionOnCreateFactory(deployProperties)) {
                ServerSession session = getServerSession();
                CommandManager rcm = session.getCommandManager();
                if (rcm != null && newSetupImpl.shouldSendMetadataRefreshCommand(deployProperties)) {
                    MetadataRefreshCommand command = new MetadataRefreshCommand(properties);
                    rcm.propagateCommand(command);
                }
                session.setRefreshMetadataListener(newSetupImpl);
            }
        } catch (RuntimeException ex) {
            if(delegate != null) {
                delegate.close();
            } else {
                newSetupImpl.undeploy();
            }
            synchronized(EntityManagerFactoryProvider.emSetupImpls){
                // bring back the old emSetupImpl and session
                EntityManagerFactoryProvider.emSetupImpls.put(sessionName, setupImpl);
                SessionManager.getManager().getSessions().put(sessionName, setupImpl.getSession());
                setupImpl.setIsMetadataExpired(false);
            }
            delegate = oldDelegate;
            throw ex;
        }
    }

    /**
     * INTERNAL: Returns the SessionBroker that the Factory will be using and
     * initializes it if it is not available. This method makes use of the
     * partially constructed session stored in our setupImpl and completes its
     * construction
     * TODO: should throw IllegalStateException if not SessionBroker
     */
    @Override
    public SessionBroker getSessionBroker() {
        return delegate.getSessionBroker();
    }

    /**
     * INTERNAL: Returns the ServerSession that the Factory will be using and
     * initializes it if it is not available. This method makes use of the
     * partially constructed session stored in our setupImpl and completes its
     * construction
     */
    @Override
    public ServerSession getServerSession() {
        return delegate.getServerSession();
    }

    /**
     * Closes this factory, releasing any resources that might be held by this
     * factory. After invoking this method, all methods on the instance will
     * throw an {@link IllegalStateException}, except for {@link #isOpen}, which
     * will return <code>false</code>.
     */
    @Override
    public synchronized void close() {
        delegate.close();
    }

    /**
     * Indicates whether or not this factory is open. Returns <code>true</code>
     * until a call to {@link #close} is made.
     */
    @Override
    public boolean isOpen() {
        return delegate.isOpen();
    }

    /**
     * PUBLIC: Returns an EntityManager for this deployment.
     */
    @Override
    public EntityManager createEntityManager() {
        return createEntityManagerImpl(null, null);
    }

    /**
     * PUBLIC: Returns an EntityManager for this deployment.
     */
    @Override
    public EntityManager createEntityManager(Map properties) {
        return createEntityManagerImpl(properties, null);
    }

    @Override
    public EntityManager createEntityManager(SynchronizationType synchronizationType) {
        return createEntityManagerImpl(null, synchronizationType);
    }

    @Override
    public EntityManager createEntityManager(SynchronizationType synchronizationType, Map map) {
        return createEntityManagerImpl(map, synchronizationType);
    }

    protected EntityManagerImpl createEntityManagerImpl(Map properties, SynchronizationType syncType) {
        EntityManagerSetupImpl setupImpl = delegate.getSetupImpl();

        if (setupImpl != null) {
            if (setupImpl.isMetadataExpired()) {
                String sessionName = setupImpl.getSessionName();
                EntityManagerSetupImpl storedImpl = null;
                synchronized (EntityManagerFactoryProvider.emSetupImpls){
                    storedImpl = EntityManagerFactoryProvider.emSetupImpls.get(sessionName);
                }
                if (storedImpl != null) {
                    delegate = new EntityManagerFactoryDelegate(storedImpl, delegate.getProperties(), this);
                }
            }
        }

        return delegate.createEntityManagerImpl(properties, syncType);
    }

    /**
     * Gets the underlying implementation of the EntityManagerFactory.
     * This method will return a version of EntityManagerFactory that is
     * based on the available metadata at the time it is called.  Future calls
     * to refresh will not affect that metadata on this EntityManagerFactory.
     */
    @Override
    public EntityManagerFactoryDelegate unwrap(){
        return delegate;
    }

    protected void verifyOpen() {
        delegate.verifyOpen();
    }

    /**
     * The method return user defined property passed in from
     * EntityManagerFactory.
     */
    public Object getProperty(String name) {
        return delegate.getProperty(name);
    }

    /**
     * Return default join existing transaction property, allows reading through
     * write connection.
     */
    public boolean getBeginEarlyTransaction() {
        return delegate.getBeginEarlyTransaction();
    }

    /**
     * Set default join existing transaction property, allows reading through
     * write connection.
     */
    public void setBeginEarlyTransaction(boolean beginEarlyTransaction) {
        delegate.setBeginEarlyTransaction(beginEarlyTransaction);
    }

    /**
     * Return default property, allows flush before query to be avoided.
     */
    public FlushModeType getFlushMode() {
        return delegate.getFlushMode();
    }

    /**
     * Set default property, allows flush before query to be avoided.
     */
    public void setFlushMode(FlushModeType flushMode) {
        delegate.setFlushMode(flushMode);
    }

    /**
     * Return default property, allows weak unit of work references.
     */
    public ReferenceMode getReferenceMode() {
        return delegate.getReferenceMode();
    }

    /**
     * Set default property, allows weak unit of work references.
     */
    public void setReferenceMode(ReferenceMode referenceMode) {
        delegate.setReferenceMode(referenceMode);
    }

    /**
     * Return default property to avoid resuming unit of work if going to be
     * closed on commit anyway.
     */
    public boolean getCloseOnCommit() {
        return delegate.getCloseOnCommit();
    }

    /**
     * Set default property to avoid resuming unit of work if going to be closed
     * on commit anyway.
     */
    public void setCloseOnCommit(boolean closeOnCommit) {
        delegate.setCloseOnCommit(closeOnCommit);
    }

    /**
     * Return default property to avoid discover new objects in unit of work if
     * application always uses persist.
     */
    public boolean getPersistOnCommit() {
        return delegate.getPersistOnCommit();
    }

    /**
     * Return interface providing access to utility methods for the persistence
     * unit.
     *
     * @return PersistenceUnitUtil interface
     * @throws IllegalStateException
     *             if the entity manager factory has been closed.
     */
    @Override
    public PersistenceUnitUtil getPersistenceUnitUtil() {
        return delegate.getPersistenceUnitUtil();
    }

    @Override
    public PersistenceUnitTransactionType getTransactionType() {
        return delegate.getTransactionType();
    }

    @Override
    public SchemaManager getSchemaManager() {
        return delegate.getSchemaManager();
    }

    /**
     * Set default property to avoid discover new objects in unit of work if
     * application always uses persist.
     */
    public void setPersistOnCommit(boolean persistOnCommit) {
        delegate.setPersistOnCommit(persistOnCommit);
    }

    /**
     * Return default property to avoid discover new objects in unit of work if
     * application always uses persist.
     */
    public boolean getCommitWithoutPersistRules() {
        return delegate.getCommitWithoutPersistRules();
    }

    /**
     * Set default property to avoid discover new objects in unit of work if
     * application always uses persist.
     */
    public void setCommitWithoutPersistRules(boolean commitWithoutPersistRules) {
        delegate.setCommitWithoutPersistRules(commitWithoutPersistRules);
    }

    /**
     * Return the default FlashClearCache mode to be used. Relevant only in case
     * call to flush method followed by call to clear method.
     *
     * @see org.eclipse.persistence.config.FlushClearCache
     */
    public String getFlushClearCache() {
        return delegate.getFlushClearCache();
    }

    /**
     * Set the default FlashClearCache mode to be used. Relevant only in case
     * call to flush method followed by call to clear method.
     *
     * @see org.eclipse.persistence.config.FlushClearCache
     */
    public void setFlushClearCache(String flushClearCache) {
        delegate.setFlushClearCache(flushClearCache);
    }

    /**
     * Return the default to determine if does-exist should be performed on
     * persist.
     */
    public boolean shouldValidateExistence() {
        return delegate.shouldValidateExistence();
    }

    /**
     * Set the default to determine if does-exist should be performed on
     * persist.
     */
    public void setShouldValidateExistence(boolean shouldValidateExistence) {
        delegate.setShouldValidateExistence(shouldValidateExistence);
    }

    /**
     * Access the cache that is associated with the entity manager
     * factory (the "second level cache").
     * @return instance of the <code>Cache</code> interface
     * @throws IllegalStateException if the entity manager factory has been closed
     * @see jakarta.persistence.EntityManagerFactory#getCache()
     * @since Java Persistence 2.0
     */
    @Override
    public Cache getCache() {
        return delegate.getCache();
    }

    /**
     * @see jakarta.persistence.EntityManagerFactory#getProperties()
     * @since Java Persistence API 2.0
     */
    @Override
    public Map<String, Object> getProperties() {
        return delegate.getProperties();
    }

    @Override
    public DatabaseSessionImpl getDatabaseSession() {
        return delegate.getDatabaseSession();
    }
    /**
     * @see jakarta.persistence.EntityManagerFactory#getCriteriaBuilder()
     * @since Java Persistence 2.0
     */
    @Override
    public CriteriaBuilder getCriteriaBuilder() {
        return delegate.getCriteriaBuilder();
    }

    /**
     * Return an instance of Metamodel interface for access to the metamodel of
     * the persistence unit.
     *
     * @return Metamodel instance
     * @throws IllegalStateException
     *             if the entity manager factory has been closed.
     * @see jakarta.persistence.EntityManagerFactory#getMetamodel()
     * @since Java Persistence 2.0
     */
    @Override
    public Metamodel getMetamodel() {
        return delegate.getMetamodel();
    }

    /**
     * INTERNAL: Convenience function to allow us to reset the Metamodel in the
     * possible case that we want to regenerate it. This function is outside of
     * the JPA 2.0 specification.
     *
     * @since Java Persistence 2.0
     */
    public void setMetamodel(Metamodel aMetamodel) {
        delegate.setMetamodel(aMetamodel);
    }

    /**
     * Determine the load state of a given persistent attribute of an entity
     * belonging to the persistence unit.
     *
     * @param entity
     *            containing the attribute
     * @param attributeName
     *            name of attribute whose load state is to be determined
     * @return false if entity's state has not been loaded or if the attribute
     *         state has not been loaded, otherwise true
     */
    @Override
    public boolean isLoaded(Object entity, String attributeName) {
        return delegate.isLoaded(entity, attributeName);
    }

    @Override
    public <E> boolean isLoaded(E e, Attribute<? super E, ?> attribute) {
        return delegate.isLoaded(e, attribute);
    }

    /**
     * Determine the load state of an entity belonging to the persistence unit.
     * This method can be used to determine the load state of an entity passed
     * as a reference. An entity is considered loaded if all attributes for
     * which FetchType EAGER has been specified have been loaded. The
     * isLoaded(Object, String) method should be used to determine the load
     * state of an attribute. Not doing so might lead to unintended loading of
     * state.
     *
     * @param entity
     *            whose load state is to be determined
     * @return false if the entity has not been loaded, else true.
     */
    @Override
    public boolean isLoaded(Object entity) {
        return delegate.isLoaded(entity);
    }

    @Override
    public void load(Object entity, String attributeName) {
        delegate.load(entity, attributeName);
    }

    @Override
    public <E> void load(E entity, Attribute<? super E, ?> attribute) {
        delegate.load(entity, attribute);
    }

    @Override
    public void load(Object entity) {
        delegate.load(entity);
    }

    @Override
    public boolean isInstance(Object entity, Class<?> entityClass) {
        return delegate.isInstance(entity, entityClass);
    }

    @Override
    public String getName() {
        return delegate.getName();
    }

    @Override
    public <T> Class<? extends T> getClass(T entity) {
        return delegate.getClass(entity);
    }

    /**
     * Returns the id of the entity. A generated id is not guaranteed to be
     * available until after the database insert has occurred. Returns null if
     * the entity does not yet have an id
     *
     * @return id of the entity
     * @throws IllegalStateException
     *             if the entity is found not to be an entity.
     */
    @Override
    public Object getIdentifier(Object entity) {
        return delegate.getIdentifier(entity);
    }

    @Override
    public Object getVersion(Object entity) {
        return delegate.getVersion(entity);
    }

    /**
     * Return if updates should be ordered by primary key to avoid possible database deadlocks.
     */
    public CommitOrderType getCommitOrder() {
        return delegate.getCommitOrder();
    }

    /**
     * Set updates should be ordered by primary key to avoid possible database deadlocks.
     */
    public void setCommitOrder(CommitOrderType commitOrder) {
        delegate.setCommitOrder(commitOrder);
    }

    @Override
    public void addNamedQuery(String name, Query query) {
        QueryImpl queryImpl = query.unwrap(QueryImpl.class);
        DatabaseQuery unwrapped = (DatabaseQuery) queryImpl.getDatabaseQueryInternal().clone();
        if (queryImpl.lockMode != null){
            ((ObjectLevelReadQuery)unwrapped).setLockModeType(queryImpl.lockMode.name(), getServerSession());
        }
        if (unwrapped.isReadQuery()){
            ((ReadQuery)unwrapped).setInternalMax((queryImpl.getMaxResultsInternal()));
            ((ReadQuery)unwrapped).setFirstResult((queryImpl.getFirstResult()));
        }
        this.getServerSession().addQuery(name, unwrapped, true);
    }

    // addNamedQuery was implemented without calling the delegate so repeating the same pattern
    @Override
    public <R> Map<String, TypedQueryReference<R>> getNamedQueries(Class<R> resultType) {
        return getNamedQueries(resultType, getServerSession());
    }

    static <R> Map<String, TypedQueryReference<R>> getNamedQueries(Class<R> resultType, AbstractSession session) {
        Map<String, List<DatabaseQuery>> queries = session.getQueries();
        Map<String, TypedQueryReference<R>> result = new HashMap<>(queries.size());
        queries.forEach((queryName, queriesList) -> {
            if (!result.containsKey(queryName)) {
                for (DatabaseQuery query : queriesList) {
                    if (query.getReferenceClass() != null && resultType.isAssignableFrom(query.getReferenceClass())) {
                        Map<String, Object> hints = QueryHintsHandler.get(query);
                        result.put(queryName,
                                   new TypedQueryReferenceImpl<>(queryName,
                                                               (Class<? extends R>) query.getReferenceClass(),
                                                               hints != null ? hints : Collections.emptyMap()));
                        break;
                    }
                }
            }
        });
        return result;
    }

    @Override
    public <T> T unwrap(Class<T> cls) {
        if (cls.equals(JpaEntityManagerFactory.class) || cls.equals(EntityManagerFactoryImpl.class)) {
            return (T) this;
        }else if (cls.equals(EntityManagerFactoryDelegate.class)) {
            return (T) this.delegate;
        }else if (cls.equals(Session.class) || cls.equals(AbstractSession.class)) {
            return (T) this.delegate.getAbstractSession();
        } else if (cls.equals(DatabaseSession.class) || cls.equals(DatabaseSessionImpl.class)) {
            return (T) this.getDatabaseSession();
        } else if (cls.equals(Server.class) || cls.equals(ServerSession.class)) {
            return (T) this.getServerSession();
        } else if (cls.equals(SessionBroker.class)) {
            return (T) this.getSessionBroker();
        }
        throw new PersistenceException(ExceptionLocalization.buildMessage("unable_to_unwrap_jpa", new String[]{EntityManagerFactory.class.getName(),cls.getName()}));
    }

    @Override
    public <T> void addNamedEntityGraph(String graphName, EntityGraph<T> entityGraph) {
        addNamedEntityGraph(graphName, entityGraph, getServerSession());
    }

    static <T> void addNamedEntityGraph(String graphName, EntityGraph<T> entityGraph, AbstractSession session) {
        AttributeGroup group = ((EntityGraphImpl)entityGraph).getAttributeGroup().clone();
        group.setName(graphName);
        session.getAttributeGroups().put(graphName, group);
        session.getDescriptor(((EntityGraphImpl)entityGraph).getClassType()).addAttributeGroup(group);
    }

    // addNamedEntityGraph was implemented without calling the delegate so repeating the same pattern
    @Override
    public <E> Map<String, EntityGraph<? extends E>> getNamedEntityGraphs(Class<E> entityType) {
        return getNamedEntityGraphs(entityType, getServerSession(), getMetamodel());
    }

    // TODO-API-3.2
    static <E> Map<String, EntityGraph<? extends E>> getNamedEntityGraphs(Class<E> entityType, AbstractSession session, Metamodel metamodel) {
        Map<String, AttributeGroup> attributeGroups = session.getAttributeGroups();
        Map<String, EntityGraph<? extends E>> result = new HashMap<>(attributeGroups.size());
        attributeGroups.forEach((name, attributeGroup) -> {
            if (attributeGroup.getType() != null && entityType.isAssignableFrom(attributeGroup.getType())) {
                ClassDescriptor descriptor = session.getDescriptor(attributeGroup.getType());
                if (descriptor == null) {
                    throw new IllegalArgumentException(ExceptionLocalization.buildMessage(
                            "jpa_non_persistent_class", new String[] {entityType.getName()}));
                }
                session.getDescriptor(entityType);
                result.put(name, new EntityGraphImpl<>(attributeGroup, descriptor));
            }
        });
        return result;
    }

    @Override
    public void runInTransaction(Consumer<EntityManager> work) {
        delegate.runInTransaction(work);
    }

    @Override
    public <R> R callInTransaction(Function<EntityManager, R> work) {
        return delegate.callInTransaction(work);
    }

}
